package org.molamil.acto.utils {
	import flash.display.DisplayObject;
	import flash.system.ApplicationDomain;
	import flash.utils.*;

	/**
	 * XXX: Document.
	 * 
	 * @author Jorge Hernandez
	 */
	public class ObjectUtils {
		
		
		// PUBLIC METHODS

		public static function clone(object:Object):* {
			var byteArray:ByteArray = new ByteArray();
			byteArray.writeObject(object);
			byteArray.position = 0;
			return byteArray.readObject();
		}

		/**
		 * XXX: Review documentation and add examples.
		 * 
		 * Merges two objects by copying all public properties from o2 onto o1.
		 * If a property from o2 already exists in o1, the latter gets overwritten.
		 * 
		 * <p>All instance, dynamic and static properties are merged. <strong>Methods and getter/setter
		 * functions are not merged.</strong></p>
		 * 
		 * @param   o1 Target object where the properties are added.
		 * @param   o2 Source object, properties from this object are added to o1.
		 * 			   This object does not get modified.
		 * @param	isConvertStatic Default value is <code>true</code>. Whether static variables in o2 are copied
		 * 							as instance or dynamic properties onto o1 when o1 does not contain declared
		 * 							static variables with matching names.
		 */
		public static function merge(o1:Object, o2:Object, isConvertStatic:Boolean = true):void {
			
			if (o1 	== null || o2 == null || o1 === o2)
				return;
			
			// Get type description.
			var c1:Class = getClass(o1);
			var c2:Class = getClass(o2);
			if (c1 	== null || c2 == null)
				return;
			var o1Description:XML = describeType(o1);
			var o2Description:XML = describeType(o2);
			var c2Description:XML = describeType(c2);
			
			// Copy all instance properties.
			for each (var variable:XML in o2Description.variable) {
				
				var name:String = variable.@name;

				// Copy from instance to instance or from instance to static.
				if (containsInstancePropertyOrAccessor(o1, name) || o1Description.@isDynamic == "true") {
					mergeOrSubmerge(o1, o2, name, isConvertStatic);
				} else if (isConvertStatic && containsStaticProperty(o1, name)) {
					mergeOrSubmerge(c1, o2, name, isConvertStatic);
				}
				
			}
			
			// Copy all static properties.
			for each (var staticVariable:XML in c2Description.variable) {
				
				var staticName:String = staticVariable.@name;
				
				// Copy from static to static or from static to instance/dynamic.
				if (containsStaticProperty(o1, staticName)) {
					mergeOrSubmerge(c1, c2, staticName, isConvertStatic);
				} else if (isConvertStatic && (containsInstancePropertyOrAccessor(o1, staticName) || o1Description.@isDynamic == "true")) {
					mergeOrSubmerge(o1, c2, staticName, isConvertStatic);
				}
					
			}
			
			// Copy all dynamic properties.
			for (var dynamicName:String in o2) {
				
				// Copy from dynamic to instance or from dynamic to static.
				if (containsInstancePropertyOrAccessor(o1, dynamicName) || o1Description.@isDynamic == "true") {
					mergeOrSubmerge(o1, o2, dynamicName, isConvertStatic);
				} else if (isConvertStatic && containsStaticProperty(o1, dynamicName)) {
					mergeOrSubmerge(c1, o2, dynamicName, isConvertStatic);
				}
				
			}
				
		}
		
		/**
		 * XXX: Review documentation and add examples.
		 * 
		 * Merges two dynamic objects by copying all dynamic properties from o2 
		 * onto o1. Instance properties, static properties, methods and 
		 * getter/setter functions are not copied.
		 * 
		 * @param   o1 Target object where the dynamic properties are copied to.
		 * @param   o2 Source object, the dynamic properties from this object are 
		 * 			   added to o1. This object does not get modified.
		 */
		public static function mergeDynamic(o1:Object, o2:Object):void {
			for (var prop:String in o2)
				o1[prop] = o2[prop];
		}
		
		public static function getClass(o:Object):Class {
			var c:Class = null;
			var className:String = getQualifiedClassName(o);
			try {
				c = getDefinitionByName(className) as Class;
			} catch (e:ReferenceError) {
				try {
					if (o is DisplayObject) {
						var applicationDomain:ApplicationDomain = DisplayObject(o).loaderInfo.applicationDomain;
						c = applicationDomain.getDefinition(className) as Class;
					}
				} catch (e1:ReferenceError) {}
			}
			return c;
		}
		
		public static function containsInstancePropertyOrAccessor(o:Object, property:String):Boolean {
			if (describeType(o).variable.(@name == property) != undefined)
				return true;
			if (describeType(o).accessor.(@name == property) != undefined &&
				describeType(o).accessor.(@name == property).@access == "readwrite") {
				return true;
			}
			return false;
		}
		
		public static function containsInstanceProperty(o:Object, property:String):Boolean {
			return describeType(o).variable.(@name == property) != undefined;
		}
		
		public static function containsStaticProperty(o:Object, property:String):Boolean {
			return describeType(getClass(o)).variable.(@name == property) != undefined;
		}
		
		public static function containsMethod(o:Object, method:String):Boolean {
			return describeType(o).method.(@name == method) != undefined;
		}
		
		public static function containsEmptyMethod(o:Object, method:String):Boolean {
			return describeType(o).method.(@name == method) != undefined 
				   && describeType(o).method.(@name == method).parameter.length() == 0;
		}
		
		public static function getPropertyType(o:Object, property:String):String {
			var type:String = describeType(o).variable.(@name == property).@type;
			return type != "" ? type : null;
		}
		
		public static function getPropertyOrAccesorType(o:Object, property:String):String {
			var type:String;
			var oDescription:XML = describeType(o);
			if (oDescription.variable.(@name == property).length() > 0) {
				type = oDescription.variable.(@name == property).@type;
			} else {
				type = oDescription.accessor.(@name == property).@type;			
			}
			return type != "" ? type : null;
		}
		
		public static function hasConstructorParameters(o:Object):Boolean {
			var b:Boolean;
			if (o is Class) {
				describeType(o).factory.constructor.parameter.length() > 0;
			} else {
				describeType(o).constructor.parameter.length() > 0;
			}
			return b;
		}
		
		public static function getNestedProperty(o:Object, nestedProperty:String):* {
			if (o == null || nestedProperty == null)
				return null;
			var v:* = o;
			var a:Array = nestedProperty.split(".");
			for (var i:uint = 0; i < a.length; i++) {
				v = v[a[i]];
				if (v == null) {
					v = null;
					break;
				}
			}
			if (v == o)
				v = null;
			return v;
		}
		
		public static function isSimple(value:Object):Boolean {
			var type:String = typeof(value);
			if (type == "number" ||type == "string" ||type == "boolean")
				return true;
			if (type == "object")
				return (value is Date) || (value is Array);
			return false;
		}
		
		public static function createInstance(type:String):Object {
			//TODO: Check for parameters in constructor.
			var newObject:Object = null;
			try {
				var c:Class = Class(getDefinitionByName(type));
				newObject = new c();
			} catch (e:Error) {
				// Empty.
			}
			return newObject;
		}
		
		public static function convertObject(o:Object, type:String):Object {
			var newObject:Object = createInstance(type);
			merge(newObject, o);
			return newObject;
		}
		
		public static function callMethod(o:Object, method:String, args:Array = null):void {
			if (containsMethod(o, method)) {
				var f:Function = o[method];
				f.apply(o, args);
			}
		}
		
		// PRIVATE METHODS
		
		private static function mergeOrSubmerge(o1:Object, o2:Object, property:String, isConvertStatic:Boolean = true):void {
			
			// Check whether it is a simple property or if it is an object with subproperties. 
			// If the case is the latter, copy the subproperties instead.
			
			if (o2[property] == null || o2[property] is Function || o2[property] is Class || isSimple(o2[property])) {
				o1[property] = o2[property];
			} else if (o1[property] == null) {
				var propertyType1:String = getPropertyOrAccesorType(o1, property);
				var is1Like2:Boolean = (propertyType1 != null) ? propertyType1 == "*" || o2[property] is (getDefinitionByName(propertyType1) as Class) : false;
				if (propertyType1 != null && !is1Like2) {
					o1[property] = convertObject(o2[property], propertyType1);
				} else {
					o1[property] = o2[property];
				}
			} else {
				merge(o1[property], o2[property], isConvertStatic);
			}
			
		}
		
		
	}
	
}